package naga.x.net.impl.websocket;

import static naga.x.net.impl.websocket.WebSocketConst.MASK;
import static naga.x.net.impl.websocket.WebSocketConst.OP_BINARY;
import static naga.x.net.impl.websocket.WebSocketConst.OP_CLOSE;
import static naga.x.net.impl.websocket.WebSocketConst.OP_MASK;
import static naga.x.net.impl.websocket.WebSocketConst.PLAYLOAD_LEN_2_BYTES;
import static naga.x.net.impl.websocket.WebSocketConst.PLAYLOAD_LEN_8_BYTES;
import static naga.x.net.impl.websocket.WebSocketConst.PLAYLOAD_LEN_MASK;

import java.util.Base64;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import naga.x.App;
import naga.x.net.codec.PacketDecoder;
import naga.x.net.codec.StickPacketDecoder;
import naga.x.util.CommonUtils;

public class WebSocketDecoder extends StickPacketDecoder implements PacketDecoder {

	private static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key:";
	private static final String HEADER_SEPARATOR = "\r\n";
	private static final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // magic number
	private static final String RESPONSE_HEADER_HANDSHAKE;
	
	protected static final String SECRET_KEY = WebSocketDecoder.class.getName() + ".seckey";
	
	protected static final Logger LOG = LoggerFactory.getLogger(WebSocketDecoder.class);
	
	static {
		StringBuilder builder = new StringBuilder(128);
		builder.append("HTTP/1.1 101 Switching Protocols").append(HEADER_SEPARATOR)
				.append("Upgrade: websocket").append(HEADER_SEPARATOR)
				.append("Connection: Upgrade").append(HEADER_SEPARATOR)
				.append("Sec-WebSocket-Origin: null").append(HEADER_SEPARATOR)
				.append("Sec-WebSocket-Location: ")
					.append(App.getApp().getConfig().sslEnable ? "wss://" : "ws://")
					.append(App.getApp().getConfig().remoteAddr)
					.append(":")
					.append(App.getApp().getConfig().port)
					.append(HEADER_SEPARATOR)
				.append("Sec-WebSocket-Accept: ");
		RESPONSE_HEADER_HANDSHAKE = builder.toString();
	}
	
	@Override
	protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
		Object buf = decode(session, in);
		if (buf != null) {
			out.write(buf);
			return true;
		}
		return false;
	}
	
	private Object doDecode0(IoSession session, IoBuffer in) {
		in.mark();
		byte op = (byte) (in.get() & OP_MASK);
		if (op == OP_CLOSE) {
			in.position(in.limit());
			session.closeNow();
			LOG.info("[WEBSOCKET CLOSE]ID[{}]IP[{}]", session.getId(), session.getRemoteAddress());
			return null;
		}
		if (!in.hasRemaining()) {
			in.reset();
			return null;
		}
		byte b = in.get();
		boolean hasMask = (b & MASK) != 0;
		int playloadLen = b & PLAYLOAD_LEN_MASK;
		if (playloadLen == PLAYLOAD_LEN_2_BYTES) {
			if (in.remaining() < 2) {
				in.reset();
				return null;
			}
			playloadLen = in.getShort();
		} else if (playloadLen == PLAYLOAD_LEN_8_BYTES) {
			if (in.remaining() < 8) {
				in.reset();
				return null;
			}
			playloadLen = (int) in.getLong();
		}
		if ((hasMask && in.remaining() < playloadLen + 4)
				|| (!hasMask && in.remaining() < playloadLen)) {
			in.reset();
			return null;
		}
		byte[] data = new byte[playloadLen];
		if (hasMask) {
			byte[] maskKey = new byte[4];
			in.get(maskKey);
			in.get(data);
			for (int i = 0; i < playloadLen; i++) {
				data[i] ^= maskKey[i & 3]; // maskKey[i % 4]
			}
		} else {
			in.get(data);
		}
		if (op == OP_BINARY) {
			return IoBuffer.wrap(data);
		}/* else if (op == OP_TEXT) {
			// TODO
		}*/
		return null;
	}
	
	private Object doHandshake(IoSession session, IoBuffer in) {
		byte[] buf = new byte[in.remaining()];
		in.get(buf);
		String headers = new String(buf);
		int from = headers.indexOf(SEC_WEBSOCKET_KEY);
		if (from < 0) {
			LOG.info("[WEBSOCKET DECODE]HANDSHAKE ERROR[" + session.getRemoteAddress().toString() + "]");
			in.position(in.limit());
			session.closeNow();
			return null;
		}
		from += SEC_WEBSOCKET_KEY.length();
		int to = headers.indexOf(HEADER_SEPARATOR, from);
		if (to < 0) {
			to = headers.length();
		}
		String secKey = headers.substring(from, to).trim() + GUID;
		// encode secKey
		byte[] sha1 = CommonUtils.encrypt("SHA-1", secKey.getBytes());
		secKey = Base64.getEncoder().encodeToString(sha1);
		session.setAttribute(SECRET_KEY, secKey);
		// response header
		headers = RESPONSE_HEADER_HANDSHAKE + secKey + HEADER_SEPARATOR + HEADER_SEPARATOR;
		buf = headers.getBytes();
		IoBuffer ioBuf = IoBuffer.wrap(buf);
		session.write(ioBuf);
		ioBuf.free();
		LOG.info("[WEBSOCKET DECODE]HANDSHAKE SUCC[" + session.getRemoteAddress().toString() + "]");
		return null;
	}

	@Override
	public Object decode(IoSession session, IoBuffer in) {
		if (session.getAttribute(SECRET_KEY) != null) {
			return doDecode0(session, in);
		}
		return doHandshake(session, in);
	}

}
